00.4 ANDROID 面向对象的六大原则——依赖倒置原则

返回设计模式博客目录
|
第一篇:单一职责原则
第二篇:开闭原则
第三篇:里氏替换原则
第四篇:依赖倒置原则
第五篇:接口隔离原则
第六篇:迪米特原则


依赖倒置原则


英文全称是 Dependence Inversion Principle,缩写 DIP。它指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次模块的实现细节,即依赖模块被颠倒了。它包含了以下几个含义:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。

在 Java 语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,可以被直接实例化 (new)。高层模块就是调用端,底层模块就是具体实现类。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。概括的说就是面向接口编程,或者说面向抽象编程,这里的抽象指的是接口或者抽象类。

如果类与类直接依赖于细节,那么它们直接就有直接的耦合,当具体实现变化时,意味着要同时修改依赖者的代码,这限制了系统的可扩展性。

在下面的代码中,ImageLoader 直接依赖于 MemoryCache,这个 MemoryCache 是一个具体实现,而不是一个抽象类或者接口。这导致了 ImageLoader 直接依赖了具体细节,当 MemoryCache 不能满足 ImageLoader 而需要被其他缓存实现替换时,此时就必须修改 ImageLoader 的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ImageLoader {
// 内存缓存(直接依赖于细节)
MemoryCache mMemoryCache = new MemoryCache();
// 加载图片到 ImageView 中
public void displayImage(String url, ImageView imageView) {
Bitmap bitmap = mMemoryCache.get(url);
if (bitmap == null) {
downloadImage(url, imageView);
} else {
imageView.setImageBitmap(bitmap);
}
}
public void setImageCache(MemoryCache cache) {
mMemoryCache = cache;
}
...
}

随着产品的升级,用户发现 MemoryCache 已经不能满足需求,用户需要的 ImageLoader 可以将图片同时缓存到内存和 SD 卡中,或者可以让用户自定义实现缓存。修改原有代码也不符合开闭原则。

正确的做法是依照依赖倒置原则依赖抽象,而不依赖具体实现。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface ImageCache {
Bitmap get(String url);
void put(String url, Bitmap bitmap);
}
public class ImageLoader {
// 图片缓存类,依赖于抽象,并且有一个默认的实现
ImageCache mImageCache = new MemoryCache();
// 加载图片到 ImageView 中
public void displayImage(String url, ImageView imageView) {
Bitmap bitmap = mMemoryCache.get(url);
if (bitmap == null) {
downloadImage(url, imageView);
} else {
imageView.setImageBitmap(bitmap);
}
}
// 注入缓存实现
public void setImageCache(ImageCache cache) {
mImageCache = cache;
}
...
}

在这里,我们建立了 ImageCache 抽象,并且让 ImageLoader 依赖于抽象而不是具体细节。当需求发生变化时,我们只需要实现 ImageCache 类或者继承其他已有的 ImageCache 子类完成相应的缓存功能,然后将具体的实现注入到 ImageLoader 即可实现缓存功能的替换,这就保证了缓存系统的可扩展性,有了拥抱变化的能力,这就是依赖倒置原则。


举例:涛哥开奔驰


先不考虑依赖倒置原则,看一下如下的设计:

从上面的类图中可以看出,司机类和奔驰车类都属于细节,并没有实现或继承抽象,它们是对象级别的耦合。通过类图可以看出司机有一个 drive() 方法,用来开车,奔驰车有一个 run() 方法,用来表示车辆运行,并且奔驰车类依赖于司机类,用户模块表示高层模块,负责调用司机类和奔驰车类。

可用以下代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Driver {
// 司机的主要职责就是驾驶汽车
public void drive(Benz benz){
benz.run();
}
}
public class Benz {
// 汽车肯定会跑
public void run() {
System.out.println("奔驰汽车开始运行...");
}
}
// 高层模块
public class Client {
public static void main(String[] args) {
Driver taoGe = new Driver();
Benz benz = new Benz();
// 司机开奔驰车
taoGe.drive(benz);
}
}

这样的设计乍一看好像也没有问题,涛哥只管开着他的奔驰车就好。但是假如有一天他不想开奔驰了,想换一辆宝马车玩玩怎么办呢?我们当然可以新建一个宝马车类,也给它弄一个 run() 方法,但问题是,这辆车有是有了,但是涛哥却不能开啊,因为司机类里面并没有宝马车的依赖。要想解决问题,只能修改代码。

1
2
3
4
5
public class BMW {
public void run(){
System.out.println("宝马汽车开始运行...");
}
}

上面的设计没有使用依赖倒置原则,我们已经发现,模块与模块之间耦合度太高,生产力太低,只要需求一变就需要大面积重构,说明这样的设计是不合理。现在我们引入依赖倒置原则,重新设计的类图如下:

可用以下代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 将司机模块抽象为一个接口
public interface IDriver {
void drive(ICar car);
}
public class Driver implements IDriver {
public void drive(ICar car){
car.run();
}
}
// 将汽车模块抽象为一个接口:可以是奔驰汽车,也可以是宝马汽车
public interface ICar {
void run();
}
public class Benz implements ICar {
public void run(){
System.out.println("奔驰汽车开始运行...");
}
}
public class BMW implements ICar {
public void run(){
System.out.println("宝马汽车开始运行...");
}
}
// 高层模块
public class Client {
public static void main(String[] args) {
IDriver taoGe = new Driver();
ICar benz = new Benz();
// 涛哥开奔驰
taoGe.drive(benz);
// 涛哥开宝马
taoGe.drive(new BMW());
}
}

如此设计,涛哥再也不怕有新车不能开的情况了。


依赖的三种方法


接口声明依赖对象: 在接口的方法中声明依赖对象,就如上面的例子。
构造函数传递依赖对象:在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface IDriver {
void drive();
}
public class Driver implements IDriver{
private ICar car;
// 构造函数注入
public Driver(ICar car){
this.car = car;
}
public void drive(ICar car){
this.car.run();
}
}

Setter 方法传递依赖对象:在抽象中设置 Setter 方法声明依赖对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface IDriver{
// 注入依赖
void setCar(ICar car);
void drive();
}
public class Driver implements IDriver {
private ICar car;
public void setCar(ICar car){
this.car = car;
}
public void drive(){
this.car.run();
}
}


深入理解


依赖倒置原则的本质就是通过抽象(抽象类或接口)使各个类或模块实现彼此独立,不互相影响,实现模块间的松耦合。在项目中使用这个规则需要以下原则:

  • 每个类尽量都要有接口或抽象类,或者抽象类和接口都有。
  • 变量的表面类型尽量是接口或者抽象类。
  • 任何类都不应该从具体类派生。
  • 尽量不要重写基类已经写好的方法(里式替换原则)。
    如果基类是一个抽象类,而这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会有一定的影响。
  • 结合里式替换原则来使用: 结合里式替换原则和依赖倒置原则我们可以得出一个通俗的规则,接口负责定义 public 属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

总结:依赖倒置原则的核心就是面向抽象(抽象类或者接口)编程